package org.saxonextensions.eml.emitter;

import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.ValidationException;
import net.sf.saxon.event.HTMLEmitter;

import javax.mail.*;
import javax.mail.internet.*;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.saxonextensions.eml.emitter.MimeMessageWrapper;

/**
 * Abstract emitter that receive a message and call {@link #messageReceived()}.
 *
 * @author Ivan Latysh IvanLatysh@yahoo.ca
 * @since 21-Jun-2004 8:05:04 PM
 */
public abstract class AbstractEmailEmitter extends HTMLEmitter {
  /** Component logger */
  protected Logger logger = Logger.getLogger(this.getClass().getName());

  // debug property
  protected static final String DEBUG = "{http://saxonextensions.org/email}debug";
  // debug folder property
  protected static final String DEBUG_FOLDER = "{http://saxonextensions.org/email}debug-output-folder";

  /** Email tags */
  protected enum EmailTag {
    FROM("email:From"),
    REPLY_TO ("email:Reply-To"),
    SUBJECT ("email:Subject"),
    TO("email:To"),
    CC("email:Cc"),
    BCC("email:Bcc"),
    PART("email:Part"),
    HEADER ("email:header"),
    CONTENT_TYPE ("email:Content-Type"),
    CONTENT_DISPOSITION ("email:Content-Disposition"),
    ATTR_CONTENT_TYPE("Content-Type"),
    ATTR_CONTENT_DISPOSITION ("Content-Disposition"),
    ATTR_NAME("name");

    public String name;

    EmailTag(String name) {
      this.name = name;
    }
  }

  /** Tag list */
  protected static final List<String> tags = new ArrayList<String>(8);
  static {
    /** email parameters tags */
    tags.add(EmailTag.FROM.name);
    tags.add(EmailTag.REPLY_TO.name);
    tags.add(EmailTag.SUBJECT.name);
    tags.add(EmailTag.TO.name);
    tags.add(EmailTag.CC.name);
    tags.add(EmailTag.BCC.name);
    tags.add(EmailTag.PART.name);
    tags.add(EmailTag.HEADER.name);
    tags.add(EmailTag.CONTENT_TYPE.name);
    tags.add(EmailTag.CONTENT_DISPOSITION.name);
  }

  /** SMTP Session */
  protected Session session;
  /** eMail message */
  protected MimeMessageWrapper message;
  /** Multipart content object */
  protected Multipart multipart;
  /** Recipients TO */
  protected List<InternetAddress> recipientsTo = new ArrayList<InternetAddress>();
  /** Recipients CC */
  protected List<InternetAddress> recipientsCc = new ArrayList<InternetAddress>();
  /** Recipients BCC */
  protected List<InternetAddress> recipientsBcc = new ArrayList<InternetAddress>();
  /** Sender eMail appear in the from field */
  protected Address sender;
  /** Reply to eMail */
  protected Address replyTo;
  /** Email subject */
  protected String subject;
  /** eMail additional headers list */
  protected Map<String, String> headers = new HashMap<String, String>();
  /** Debug dump folder */
  protected File debugDumpFolder;
  /** Debug flag */
  protected Boolean debug = false;

  /** Writer */
  protected CharArrayWriter writer = new CharArrayWriter();

  /** Indicate that constructed document is a valid email */
  protected boolean valid = true;

  /** Attributes map */
  protected HashMap<String, HashMap<String, String>> attributes = new HashMap<String, HashMap<String, String>>();

  /**
   * Start document
   */
  public void startDocument(int properties) throws XPathException {
    super.startDocument(properties);
    try {
      // get output properties
      Properties outProps = getOutputProperties();
      // get debug property
      debug = Boolean.valueOf(outProps.getProperty(DEBUG));
      // prepare debug eml folder
      String dumpFolder = outProps.getProperty(DEBUG_FOLDER);
      if (null!=dumpFolder) {
        // use specified debug folder
        debugDumpFolder = new File(outProps.getProperty(DEBUG_FOLDER));
      } else {
        // use current directory
        debugDumpFolder = new File(System.getProperty("user.dir"));
      }
      if (!debugDumpFolder.exists()) debugDumpFolder.mkdirs();
      if (debugDumpFolder.isFile()) throw new Exception("Debug folder {"+debugDumpFolder.getAbsolutePath()+"} is a file.");
      // Set debug level
      // if (debug) logger.setLevel(Level.FINEST);
      // get message mime-type
      String mimeType = outProps.getProperty(EmailTag.ATTR_CONTENT_TYPE.name);

      if (null==session) {
        logger.log(Level.FINEST, "Getting dummy SMTP Session for localhost.");
        session = getSession();
      } else {
        logger.log(Level.FINEST, "Session has been instantiated before, will use it {"+session+"}");
      }

      // Create new MIME message for this session
      message = new MimeMessageWrapper(session);
      // create multipart message content
      multipart = (null!=mimeType && mimeType.trim().length()>0 ?new MimeMultipart(mimeType) :new MimeMultipart());

      // set custom writer
      setWriter(writer);

    } catch (Exception ex) {
      logger.log(Level.SEVERE, "Unable to start the document.", ex);
      valid = false;
      throw new ValidationException(ex);
    }
  }

  /**
   * Closing start tag
   *
   * @throws net.sf.saxon.trans.XPathException
   */
  public void closeStartTag() throws XPathException {
    String dName = null;
    try {
      // skip close tag for text node
      dName = (String) elementStack.peek();
      // close the tag
      super.closeStartTag();
      // reset writer after any email tag
      if (tags.indexOf(dName)!=-1) writer.reset();
    } catch (XPathException e) {
      logger.log(Level.SEVERE, "Unable to close the tag {"+dName+"}", e);
      valid = false;
    }
  }

  protected void writeAttribute(int elCode, String attname, CharSequence value, int properties) throws XPathException {
    try {
      addAttribute(elCode, attname, value);
    } catch (Exception ex) {
      logger.log(Level.SEVERE, "Unable to save attribute {"+attname+"} with value {"+String.valueOf(value)+"}", ex);
      valid = false;
    }
    super.writeAttribute(elCode, attname, value, properties);
  }

  /**
   * End of an element.
   */
  public void endElement() throws XPathException {
    // retrieve element name
    final String element =  (String) elementStack.peek();
    if (tags.indexOf(element)!=-1) {
      try {
        MimeBodyPart mimePart = null;
        // pop the tag from the stack
        elementStack.pop();

        // Body part
        if (element.equals(EmailTag.PART.name)) {
          // get content type
          String contentType = getAttributeValue(element, EmailTag.ATTR_CONTENT_TYPE.name);
          String disposition = getAttributeValue(element, EmailTag.ATTR_CONTENT_DISPOSITION.name);
          try {
            // Text mime body part
            mimePart = new MimeBodyPart();
            // if no content type is given, using `text/plain`
            if (null==contentType || contentType.trim().length()==0) contentType="text/plain";
            // set part content-type
            mimePart.setContent(writer.toString(), contentType);
            // set content disposition, if given
            if (null!=disposition && disposition.trim().length()>0) mimePart.setDisposition(disposition);
          } catch (MessagingException e) {
            logger.log(Level.SEVERE, "Unable to add a mime part {ContentType={"+contentType+"}", e);
            throw e;
          }
        }
        // From address
        else if (element.equals(EmailTag.FROM.name)) {
          final String senderName = getAttributeValue(element, EmailTag.ATTR_NAME.name);
          if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "From : "+(null!=senderName? "<"+senderName+">" :"")+writer.toString());
          try {
            if (null!=senderName) {
              sender = new InternetAddress(writer.toString(), senderName);
            } else {
              sender = new InternetAddress(writer.toString());
            }
          } catch (AddressException e) {
            logger.log(Level.SEVERE, "Can't construct an email address from given recipient {"+writer.toString()+"}");
            throw e;
          }
        }
        // Reply TO address
        else if (element.equals(EmailTag.REPLY_TO.name)) {
          final String replyName = getAttributeValue(element, EmailTag.ATTR_NAME.name);
          if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "Reply-To : "+(null!=replyName? "<"+replyName+">" :"")+writer.toString());
          try {
            if (null!=replyName) {
              replyTo = new InternetAddress(writer.toString(), replyName);
            } else {
              replyTo = new InternetAddress(writer.toString());
            }
          } catch (AddressException e) {
            logger.log(Level.SEVERE, "Can't construct an email address from given recipient {"+writer.toString()+"}");
            throw e;
          }
        }
        // Subject
        else if (element.equals(EmailTag.SUBJECT.name)) {
          if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "Subject : "+writer.toString());
          subject = writer.toString();
        }
        // Recipients TO
        else if (element.equals(EmailTag.TO.name)) {
          final String recipientName = getAttributeValue(element, EmailTag.ATTR_NAME.name);
          if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "To : "+(null!=recipientName? "<"+recipientName+">" :"")+writer.toString());
          try {
            if (null!=recipientName) {
              recipientsTo.add(new InternetAddress(writer.toString(), recipientName));
            } else {
              recipientsTo.add(new InternetAddress(writer.toString()));
            }
          } catch (AddressException e) {
            logger.log(Level.SEVERE, "Can't construct an email address from given string {"+writer.toString()+"}", e);
            throw e;
          }
        }
        // Recipients CC
        else if (element.equals(EmailTag.CC.name)) {
          final String recipientName = getAttributeValue(element, EmailTag.ATTR_NAME.name);
          if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "Cc : "+(null!=recipientName? "<"+recipientName+">" :"")+writer.toString());
          try {
            if (null!=recipientName) {
              recipientsCc.add(new InternetAddress(writer.toString(), recipientName));
            } else {
              recipientsCc.add(new InternetAddress(writer.toString()));
            }
          } catch (AddressException e) {
            logger.log(Level.SEVERE, "Can't construct an email address from given string {"+writer.toString()+"}", e);
            throw e;
          }
        }
        // Recipients BCC
        else if (element.equals(EmailTag.BCC.name)) {
          final String recipientName = getAttributeValue(element, EmailTag.ATTR_NAME.name);
          if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "Bcc : "+(null!=recipientName? "<"+recipientName+">" :"")+writer.toString());
          try {
            if (null!=recipientName) {
              recipientsBcc.add(new InternetAddress(writer.toString(), recipientName));
            } else {
              recipientsBcc.add(new InternetAddress(writer.toString()));
            }
          } catch (AddressException e) {
            logger.log(Level.SEVERE, "Can't construct an email address from given string {"+writer.toString()+"}", e);
            throw e;
          }
        }
        // Additional headers
        else if (element.equals(EmailTag.HEADER.name)) {
          final String headerName = getAttributeValue(element, EmailTag.ATTR_NAME.name);
          if (logger.isLoggable(Level.FINEST)) logger.log(Level.FINEST, "Header : "+(null!=headerName ? "<"+headerName +">" :"")+writer.toString());
          if (null!=headerName) {
            headers.put(headerName, writer.toString());
          }
        }

        // Add text part to multipart content object
        if (null != mimePart) {
          multipart.addBodyPart(mimePart);
        }

        // reset writer
        writer.reset();

      } catch (Exception e) {
        logger.log(Level.SEVERE, "Error while constructing email message.", e);
        valid = false;
      }
    } else {
      super.endElement();
    }
    try {
      // remove element attributes
      removeAttributes(element);
    } catch (Exception ex) {
      logger.log(Level.SEVERE, "Unable to remove the attributes for component with name {"+element+"}", ex);
      valid = false;
    }
  }

  /**
   * End of document
   */
  public void endDocument() throws XPathException {
    super.endDocument();
    boolean dump = debug;
    try {
      if (!valid) throw new Exception("Constructed message is not a valid message.");
      logger.log(Level.FINEST, "Setting message parameters.");

      // set message sender
      message.setSender(sender);
      
      // set from address
      message.setFrom(sender);

      // set messae subject
      message.setSubject(subject);

      // set recipients TO
      if (recipientsTo.size()>0) {
        logger.log(Level.FINEST, "Setting recipients TO");
        InternetAddress[] recpt = new InternetAddress[recipientsTo.size()];
        recipientsTo.toArray(recpt);
        message.setRecipients(Message.RecipientType.TO, recpt);
      }

      // set recipients CC
      if (recipientsCc.size()>0) {
        InternetAddress[] recpt = new InternetAddress[recipientsCc.size()];
        recipientsCc.toArray(recpt);
        message.setRecipients(Message.RecipientType.CC, recpt);
      }

      // set recipients BCC
      if (recipientsBcc.size()>0) {
        InternetAddress[] recpt = new InternetAddress[recipientsBcc.size()];
        recipientsBcc.toArray(recpt);
        message.setRecipients(Message.RecipientType.BCC, recpt);
      }
      
      // Set current date as the sent date
      message.setSentDate(new Date());

      // set message additional header
      for (Map.Entry<String, String> entry: headers.entrySet()) {
        message.addHeader(entry.getKey(), entry.getValue());
      }

      logger.log(Level.FINEST, "Setting content");
      
      // Set message content
      message.setContent(multipart);

      // call message received method
      messageReceived();

    } catch (Exception e) {
      if (null!=logger) logger.log(Level.SEVERE, "Error sending the message ["+message.getMessageNumber()+"].", e);
      dump = true;
    } finally {
      if (dump) {
        try {
          final String recipient = (recipientsTo.size()>0 && null!= recipientsTo.get(0) ? recipientsTo.get(0).getAddress() :"unknown-recipinet");
          dumpMessage(message.getMessage(), File.createTempFile(recipient.replaceAll("[^a-zA-Z0-9]", "_")+"-", ".eml", debugDumpFolder));
        } catch (IOException e1) {
          logger.log(Level.SEVERE, "Unable to dump the message.", e1);
        }
      }
    }
  }

  /**
   * Lookup and return a display name for the component with given code
   *
   * @param elCode component code
   * @return display name or <code>null</code>
   */
  protected String lookupDisplayName(int elCode) {
    // look up name in a pool
    return namePool.getDisplayName(elCode);
  }

  /**
   * Add attribute to the attribute array
   *
   * @param elCode element code
   * @param attname attribute name
   * @param value value
   */
  protected void addAttribute(int elCode, String attname, CharSequence value) {
    final String name = lookupDisplayName(elCode);

    if (null!=name) {
      if (attributes.containsKey(name)) {
        HashMap<String, String> attMap = attributes.get(name);
        attMap.put(attname, String.valueOf(value));
      } else {
        HashMap<String, String> attMap = new HashMap<String, String>(1);
        attMap.put(attname, String.valueOf(value));
        attributes.put(name, attMap);
      }

    } else {
      logger.log(Level.SEVERE, "Unable to lookup a display name for element with code {"+elCode+"}");
    }
  }

  /**
   * Remove all attributes for given element code
   *
   * @param elementName element name
   */
  protected void removeAttributes(String elementName) {
    attributes.remove(elementName);
  }

  /**
   * Return attribute value
   *
   * @param elementName element name
   * @param attribute attribute name to return value for
   * @return attribute value, or <code>null</code> when element have no such an attribute
   */
  protected String getAttributeValue (String elementName, String attribute) {
    if (null!=elementName) {
      if (attributes.containsKey(elementName)) {
        final HashMap attMap = attributes.get(elementName);
        if (attMap.containsKey(attribute)) {
          return String.valueOf(attMap.get(attribute));
        }
      }
    }
    return null;
  }

  /**
   * Dump given message into given file
   *
   * @param message message to dump to file
   * @param file file to dump message to
   */
  protected void dumpMessage(Message message, File file) {
    if (null!=message) {
      FileOutputStream msgDumpOut = null;
      try {
        if (!file.exists()) file.createNewFile();
        msgDumpOut = new FileOutputStream(file);
        message.writeTo(msgDumpOut);
      } catch (Exception e) {
        logger.log(Level.SEVERE, "Unable to save message to file {"+file.getAbsolutePath()+"}", e);
      } finally {
        if (null!=msgDumpOut) try {
          msgDumpOut.close();
        } catch (IOException e) {
          logger.log(Level.SEVERE, "Unable to close message output stream.", e);
        }
      }
    }
  }

  @Override
  public String getSystemId() {
    return String.valueOf(System.currentTimeMillis());
  }

  /**
   * Method get called when message is received
   */
  protected abstract void messageReceived() throws Exception;

  /**
   * Return a sesson to be used to construct the message
   *
   * @return session
   * @throws Exception when unable to return a session
   */
  protected abstract Session getSession() throws Exception;

}
